"""
邢不行™️ 策略分享会
仓位管理实盘框架

版权所有 ©️ 邢不行
微信: xbx6660

本代码仅供个人学习使用，未经授权不得复制、修改或用于商业用途。

Author: 邢不行
"""
import gc
import locale
import os.path
import platform
import random
import shutil
import sys
import time
import traceback
import warnings
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timedelta

import pandas as pd

from config import is_debug, error_webhook_url, snapshot_path, clean_start
from core.account_manager import init_system
from core.model.backtest_config import MultiEquityBacktestConfig
from core.real_trading import run_by_account
from core.utils.commons import next_run_time, sleep_until_run_time, remedy_until_run_time
from core.utils.datatools import check_bmac_update_flag
from core.utils.dingding import send_wechat_work_msg
from core.utils.functions import refresh_diff_time
from core.utils.log_kit import logger, divider
from core.utils.misc_kit import mem_save_exe
from core.utils.path_kit import get_file_path
from core.version import version_prompt

# ====================================================================================================
# ** 脚本运行前配置 **
# 主要是解决各种各样奇怪的问题们
# ====================================================================================================
# region 脚本运行前准备
warnings.filterwarnings('ignore')  # 过滤一下warnings，不要吓到老实人

# pandas相关的显示设置，基础课程都有介绍
pd.set_option('display.max_rows', 1000)
pd.set_option('expand_frame_repr', False)  # 当列太多时不换行
pd.set_option('display.unicode.ambiguous_as_wide', True)  # 设置命令行输出时的列对齐功能
pd.set_option('display.unicode.east_asian_width', True)

# 解决pm2在win系统下的乱码问题
os_type = platform.system()
if os_type == 'Windows':  # win执行
    # noinspection PyUnresolvedReferences
    sys.stdout.reconfigure(encoding='utf-8')
    # 若上面sys.stdout = ... 这一行不行，可以试试下一行
    # import io
    # sys.stdout = io.TextIOWrapper(sys.stdout.detach(), encoding='utf-8')

# 防止部分操作系统，无法识别中文名的策略或者因子
locale.setlocale(locale.LC_ALL, 'en_US.UTF-8')
# endregion


# ====================================================================================================
# ** 本脚本的全局变量 **
# ====================================================================================================
# 构建线程池，给统计脚本异步执行
stat_executor = ThreadPoolExecutor()  # 统计脚本真的会跑很久很久
python_exec = sys.executable  # 获取当前环境下的python解释器，也是给统计脚本用的


# ====================================================================================================
# ** 重要的函数区域 **
# 负责我们保温杯系统平顺执行的核心组件
# ====================================================================================================
def run_statistics_async(run_time, random_time):
    divider('运行开始统计', '-')
    logger.info(f'sleep 一下保证统计脚本在 offset 下单之后开始计算数据···')
    time.sleep(random_time if random_time else 120)  # 提前随机多久，就休息多久，默认2分钟
    os.system(f'{python_exec} {get_file_path("core", "utils", "statistics.py")} {int(run_time.timestamp())}')
    divider('统计运行结束', '-')


def run_loop() -> datetime | None:
    """
    无限循环这个函数♾️
    :return: 成功执行的话，返回本次运行的时间，否则是None
    """
    divider('本次循环开始', '+')

    # ====================================================================================================
    # 0. 调试相关配置区域
    # ====================================================================================================
    if is_debug:  # 调试模式，不进行sleep，直接继续往后运行
        random_time = None
        from config import account_config
        hour_offset_min = int(account_config['hour_offset'][:-1])
        run_time = next_run_time('1h', 0) - timedelta(hours=1) + timedelta(minutes=hour_offset_min)
        if run_time > datetime.now():
            run_time -= timedelta(hours=1)
    else:
        random_time = random.randint(1 * 60, 2 * 60)  # 不建议使用负数。小时级别提前时间可以设置短一点
        run_time = sleep_until_run_time('5m', if_sleep=True, cheat_seconds=random_time)  # 每五分钟运行一次

    # ====================================================================================================
    # 1. 更新、检查账户信息
    # ====================================================================================================
    account_config = init_system()
    me_conf = MultiEquityBacktestConfig()
    if is_debug:
        logger.debug('🐞 调试模式 - 不更新账户信息')
    else:
        account_config.use_spot = me_conf.use_spot
        account_config.update_account_info()

    # ====================================================================================================
    # 2. 等待数据中心完成数据更新
    # ====================================================================================================
    logger.info(f'检查数据中心，{run_time}...')
    # 检查数据中心数据是否更新
    is_data_ready = check_bmac_update_flag(run_time)
    # 判断数据是否更新好了
    if not is_data_ready:  # 没有更新好，跳过当前下单
        logger.warning(f'数据中心数据未更新，跳过当前，{run_time}，[调试模式]：{is_debug}')
        return
    logger.ok(f'数据中心数据更新完成，准备选币下单，[调试模式]：{is_debug}')

    # ====================================================================================================
    # 3. 针对每一个配置好的可用账户，进行因子计算、选币、下单
    # ====================================================================================================
    """!!! 核心逻辑在这里 !!!"""
    mem_save_exe(run_by_account, account_config, me_conf, run_time)
    gc.collect()  # 强制垃圾回收

    # 本次循环结束
    divider('本次循环结束，23秒后进入下一次循环', '+')
    time.sleep(23)

    # ====================================================================================================
    # 4. 运行当前offset的监测脚本
    # ====================================================================================================
    if run_time.minute in [account_config.hour_offset_minute] and not is_debug:
        stat_executor.submit(run_statistics_async, run_time, random_time)  # 异步执行

    remedy_until_run_time(run_time)  # 休息休息～～～
    return run_time


def check_accounts():
    # 针对所有的账户，进行运行前准备
    account = init_system()

    logger.debug('%s 检查杠杆、持仓模式、保证金模式...' % account.name)
    account.bn.reset_max_leverage()  # 检查杠杆
    account.bn.set_single_side_position()  # 检查并且设置持仓模式：单向持仓
    account.bn.set_multi_assets_margin()  # 检查联合保证金模式
    time.sleep(2)  # 多账户之间，停顿一下
    logger.ok('自检完成')


if __name__ == '__main__':
    version_prompt()
    if is_debug:
        print('🟠' * 16, '[调试模式]', '🟠' * 16)
    else:
        print('🟢' * 16, '[正式模式]', '🟢' * 16)
    logger.info(f'系统初始化，稍等...')
    time.sleep(1)

    # ====================================================================================================
    # 运行前自检和准备
    # ====================================================================================================
    # 如果不是debug模式准备账户设置
    if clean_start:
        shutil.rmtree(snapshot_path, ignore_errors=True)
    else:
        logger.critical('!!!!! 注意，你选择了允许缓存的条件下重启，可能会有灾难性后果 !!!!!')
        logger.warning('如果你对此不确定，请立即退出，并删除 `data -> snapshot` 文件夹，再重启。')
        logger.debug('给你7s时间确认...')
        time.sleep(7)

    if is_debug:
        logger.debug('🐞 调试模式 - 跳过账户检查和准备')
    else:
        snapshot_path.mkdir(parents=True, exist_ok=True)
        refresh_diff_time()  # 刷新与交易所的时差
        check_accounts()

    # ====================================================================================================
    # 实盘主程序，电不停我不停
    # ====================================================================================================
    while True:
        try:
            run_loop()

            if is_debug:
                break
        except Exception as err:
            msg = '系统出错，10s之后重新运行，出错原因: ' + str(err)
            logger.error(msg)
            logger.debug(traceback.format_exc())
            send_wechat_work_msg(msg, error_webhook_url)
            time.sleep(11)  # 休息十一秒钟，再冲
